package xyz.lwm.lazycat.web.resolve;

import lombok.AllArgsConstructor;
import xyz.lwm.lazycat.utility.Assert;
import xyz.lwm.lazycat.utility.CollectionUtil;
import xyz.lwm.lazycat.utility.DateUtil;
import xyz.lwm.lazycat.utility.It;
import xyz.lwm.lazycat.utility.StringUtil;
import xyz.lwm.lazycat.utility.convert.Convert;
import xyz.lwm.lazycat.utility.convert.Converter;
import xyz.lwm.lazycat.utility.reflect.AnnotationUtil;
import xyz.lwm.lazycat.utility.reflect.Annotations;
import xyz.lwm.lazycat.utility.reflect.ReflectUtil;
import xyz.lwm.lazycat.web.WebException;
import xyz.lwm.lazycat.web.annocation.Body;
import xyz.lwm.lazycat.web.annocation.Cookie;
import xyz.lwm.lazycat.web.annocation.Form;
import xyz.lwm.lazycat.web.annocation.MediaType;
import xyz.lwm.lazycat.web.annocation.Param;
import xyz.lwm.lazycat.web.annocation.PathParam;
import xyz.lwm.lazycat.web.annocation.Produces;
import xyz.lwm.lazycat.web.handle.Context;
import xyz.lwm.lazycat.web.handle.Handler;
import xyz.lwm.lazycat.web.handle.HandlerType;
import xyz.lwm.lazycat.web.handle.HttpRequest;
import xyz.lwm.lazycat.web.handle.HttpResponse;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.net.HttpCookie;
import java.util.Date;
import java.util.Map;

/**
 * 路由注解解析
 *
 * @author lwm
 */
@AllArgsConstructor
class MethodHandler implements Handler {

    // GET POST PUT DELETE PATCH HEAD
    private final HandlerType handlerType;

    // controller instance
    private final Object controller;

    // http operation
    private final Method method;

    // parameters of method
    private final Parameter[] parameters;

    // produces
    private final It<Produces> produces;

    @Override
    public void handle(Context ctx) {
        // 解析参数
        Object[] args;
        if (CollectionUtil.isEmpty(parameters)) {
            args = new Object[0];
        } else {
            args = new Object[parameters.length];
            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                args[i] = getParamValue(parameter, ctx);
            }
        }

        // 执行方法
        Object result = ReflectUtil.invoke(controller, method, args);

        // 响应对象
        HttpResponse response = ctx.res();
        // 响应状态和响应类型 处理
        produces.let(Produces::statusCode)
                .orElse(() -> handlerType == HandlerType.POST ? 201 : 200)
                .filter(c -> !response.hasSetStatus())
                .also(response::setStatus);
        It<String> mediaType = produces.let(Produces::value)
                .orElse(MediaType.APPLICATION_JSON)
                .also(response::setContentType);
        // 结果写出
        if (MediaType.APPLICATION_JSON.equals(mediaType.get())) {
            if (result != null) {
                ctx.json(result);
            }
        } else {
            response.write(String.valueOf(result));
        }

    }

    /**
     * 获取 controller 方法的参数值
     *
     * @param parameter parameter
     * @param ctx       context
     * @return parameter value
     */
    private Object getParamValue(Parameter parameter, Context ctx) {
        Class<?> type = parameter.getType();
        if (type.isAssignableFrom(HttpRequest.class)) {
            return ctx.req();
        } else if (type.isAssignableFrom(HttpResponse.class)) {
            return ctx.res();
        } else {
            return resolveParameterValue(parameter, ctx);
        }
    }

    private Object resolveParameterValue(Parameter parameter, Context ctx) {
        Annotations annotations = AnnotationUtil.getAnnotations(parameter);
        checkAnnotations(annotations);

        Cookie cookie = annotations.getAnnotation(Cookie.class);
        if (cookie != null) {
            return It.of(ctx.req().getCookie(cookie.name())).let(HttpCookie::getValue)
                    .let(v -> Convert.fromString(v, parameter.getType())).getOr(null);
        }

        PathParam pathParam = annotations.getAnnotation(PathParam.class);
        if (pathParam != null) {
            return Convert.fromString(ctx.req().getPathParam(It.of(pathParam.name()).getOrThrow(() ->
                    new WebException("pathParam name can not be empty"))), parameter.getType());
        }

        Param param = annotations.getAnnotation(Param.class);
        if (param != null) {
            return getParamObject(param, parameter, ctx.req());
        }

        Form form = annotations.getAnnotation(Form.class);
        if (form != null) {
            return getFormObject(parameter, ctx.req());
        }

        Body body = annotations.getAnnotation(Body.class);
        if (body != null) {
            return getBodyObject(parameter, ctx);
        }

        return null;
    }

    private Object getParamObject(Param param, Parameter parameter, HttpRequest req) {
        Assert.isTrue(StringUtil.isNotBlank(param.name()), "param name cannot be blank");

        String reqParam = req.getParam(param.name());

        Class<? extends Converter> converterClass = param.converter();
        if (converterClass != Converter.class) {
            int modifiers = converterClass.getModifiers();
            if (Modifier.isPublic(modifiers) &&
                    !Modifier.isAbstract(modifiers) &&
                    !Modifier.isInterface(modifiers)) {
                Converter<?> converter = ReflectUtil.newInstance(converterClass);
                return converter.fromString(reqParam);
            }
        }

        String dateFormat = param.dateFormat();
        if (StringUtil.isNotBlank(dateFormat) && StringUtil.isNotBlank(reqParam)) {
            Date date = DateUtil.parse(reqParam, dateFormat);
            reqParam = DateUtil.format(date, DateUtil.DEFAULT_PATTERN);
        }

        return Convert.fromString(reqParam, parameter.getType());
    }

    private Object getFormObject(Parameter parameter, HttpRequest req) {
        // form object
        Object form = ReflectUtil.newInstance(parameter.getType());
        Map<String, Field> fields = ReflectUtil.findDeclaredFields(parameter.getType());
        fields.forEach((s, field) -> {
            It<Annotations> it = It.of(AnnotationUtil.getAnnotations(field));
            // @Param 处理, 优先取注解的名称, 否则取字段名称, 然后从 req 中获取值进行转换, 最后赋值给字段
            it.let(a -> a.getAnnotation(Param.class))
                    .let(Param::name).filter(StringUtil::isNotBlank).orElse(field::getName)
                    .let(nm -> Convert.fromString(req.getParam(nm), field.getType()))
                    .also(obj -> ReflectUtil.setValue(form, field, obj));
            // @PathParam 处理
            it.let(a -> a.getAnnotation(PathParam.class))
                    .let(PathParam::name).filter(StringUtil::isNotBlank).orElse(field::getName)
                    .let(nm -> Convert.fromString(req.getPathParam(nm), field.getType()))
                    .also(obj -> ReflectUtil.setValue(form, field, obj));
            // @Cookie 处理
            it.let(a -> a.getAnnotation(Cookie.class))
                    .let(Cookie::name).filter(StringUtil::isNotBlank).orElse(field::getName)
                    .let(req::getCookie).let(HttpCookie::getValue)
                    .also(obj -> ReflectUtil.setValue(form, field, obj));
        });
        return form;
    }

    private Object getBodyObject(Parameter parameter, Context ctx) {
        if (ctx.req().isApplicationJson()) {
            return ctx.bodyAsClass(parameter.getType());
        }
        if (ctx.req().isTextPlain() && CharSequence.class.isAssignableFrom(parameter.getType())) {
            return ctx.req().getBody();
        }
        throw new WebException("Media type should be 'application/json' or parameter type should be String");
    }

    private void checkAnnotations(Annotations annotations) {
        int count = 0;
        if (annotations.hasAnnotation(PathParam.class) ||
                annotations.hasAnnotation(Param.class) ||
                annotations.hasAnnotation(Form.class) ||
                annotations.hasAnnotation(Body.class) ||
                annotations.hasAnnotation(Cookie.class)) {
            count++;
        }
        if (count != 1) {
            throw new RuntimeException("There can only be one annotation form @PathParam, @PathParam, @Form, @Body, @Cookie.");
        }
    }

}
